//
//  LSSyncEngine.m
//  LibraryStorm
//
//  Created by Enel@Lightning on 3/26/14.
//  Copyright (c) 2014 3ATeam. All rights reserved.
//

#import "LSSyncEngine.h"
#import "LSAPIClient.h"
#import "LSCoreDataController.h"
#import "BookCategory.h"
#import "BookDetail.h"

NSString * const kLSSyncEngineInitialCompleteKey = @"LSSyncEngineInitialSyncCompleted";
NSString * const kLSSyncEngineSyncCompletedNotificationName = @"LSSyncEngineSyncCompleted";

@interface LSSyncEngine ()

@property (nonatomic, strong) NSMutableArray *registeredClassesToSync;
@end

@implementation LSSyncEngine
@synthesize registeredClassesToSync = _registeredClassesToSync;

+ (LSSyncEngine *)sharedEngine
{
    static LSSyncEngine *sharedEngine = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedEngine = [[LSSyncEngine alloc] init];
    });
    
    return sharedEngine;
}

#pragma mark - Register Class To Sync (Start when lauch app)
- (void)registerNSManagedObjectClassToSync:(Class)aClass
{
    if (!self.registeredClassesToSync) {
        self.registeredClassesToSync = [NSMutableArray array];
    }
    
    if ([aClass isSubclassOfClass:[NSManagedObject class]]) {
        if (![self.registeredClassesToSync containsObject:NSStringFromClass(aClass)]) {
            [self.registeredClassesToSync addObject:NSStringFromClass(aClass)];
        } else {
            NSLog(@"Unable to register %@ as it is already registered", NSStringFromClass(aClass));
        }
    } else {
        NSLog(@"Unable to register %@ as it is not a subclass of NSManagedObject", NSStringFromClass(aClass));
    }
}

#pragma mark - Sync New, Update, LoadMore Records
- (void)startSync
{
    if (!self.syncInProgress) {
        [self willChangeValueForKey:@"syncInProgress"];
        _syncInProgress = YES;
        [self didChangeValueForKey:@"syncInProgress"];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
            [self downloadDataForRegisteredObjects:YES toDeleteLocalRecords:NO];
        });
    }
}

- (void)syncDataForRegisteredObject:(NSString *)className toUseUpdatedAtDate:(BOOL)useUpdatedAtDate toDeleteLocalRecords:(BOOL)toDelete withCategoryType:(int)categoryId limitRecords:(int)limit skipOverRecords:(int)skip
{
    if (!self.syncInProgress) {
        [self willChangeValueForKey:@"syncInProgress"];
        _syncInProgress = YES;
        [self didChangeValueForKey:@"syncInProgress"];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
            [self downloadDataForRegisteredObject:className toUseUpdatedAtDate:useUpdatedAtDate toDeleteLocalRecords:toDelete withCategoryType:categoryId limitRecords:limit skipOverRecords:skip];
        });
    }
}

#pragma mark - Download Data
- (void)downloadDataForRegisteredObjects:(BOOL)useUpdatedAtDate toDeleteLocalRecords:(BOOL)toDelete
{
    // Create a dispatch group
    dispatch_group_t group = dispatch_group_create();
    for (NSString *className in self.registeredClassesToSync) {
        // Enter the group for each request we create
        dispatch_group_enter(group);
        NSDate *mostRecentUpdatedDate = nil;
        if (useUpdatedAtDate) {
            mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className];
        }
        NSMutableURLRequest *request = [[LSAPIClient sharedClient]
                                        GETRequestForAllRecordsOfClass:className
                                        updatedAfterDate:mostRecentUpdatedDate limitRecords:LIMIT_MAX_RECORDS skipOverRecords:0];
        AFHTTPRequestOperation *operation = [[LSAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
            if ([responseObject isKindOfClass:[NSDictionary class]]) {
                [self writeJSONResponse:responseObject toDiskForClassWithName:className];
                // Leave the group as soon as the request succeeded
                dispatch_group_leave(group);
            }
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            NSLog(@"Request for class %@ failed with error: %@", className, error);
            // Leave the group as soon as the request succeeded
            dispatch_group_leave(group);
        }];
        
        [[LSAPIClient sharedClient].operationQueue addOperation:operation];
    }

    // Here we wait for all the requests to finish
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // Do whatever you need to do when all requests are finished
        if ([LSAPIClient sharedClient].operationQueue.operationCount == 0) {
            if (!toDelete) {
                [self processJSONDataRecordsIntoCoreData];
            }else {
                [self processJSONDataRecordsForDeletion];
            }
        }
    });
}

- (void)downloadDataForRegisteredObject:(NSString *)className toUseUpdatedAtDate:(BOOL)useUpdatedAtDate toDeleteLocalRecords:(BOOL)toDelete withCategoryType:(int)categoryId limitRecords:(int)limit skipOverRecords:(int)skip
{
    NSDate *mostRecentUpdatedDate = nil;
    if (useUpdatedAtDate) {
        mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className];
    }
    
    NSMutableURLRequest *request = [[LSAPIClient sharedClient] GETRequestForAllRecordsOfClass:className updatedAfterDate:mostRecentUpdatedDate categoryType:categoryId limitRecords:limit skipOverRecords:skip];
    
    AFHTTPRequestOperation *operation = [[LSAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
        if ([responseObject isKindOfClass:[NSDictionary class]]) {
            [self writeJSONResponse:responseObject toDiskForClassWithName:className];
            
            [self processJSONDataRecordsIntoCoreData:className toUseUpdatedAtDate:useUpdatedAtDate];
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Request for class %@ failed with error: %@", className, error);
        [self willChangeValueForKey:@"syncInProgress"];
        _syncInProgress = NO;
        [self didChangeValueForKey:@"syncInProgress"];
        
        if ([self.delegate respondsToSelector:@selector(syncEngine:withError:toUseUpdatedAtDate:)]) {
            [self.delegate syncEngine:self withError:error toUseUpdatedAtDate:useUpdatedAtDate];
        }
    }];
    
    [[LSAPIClient sharedClient].operationQueue addOperation:operation];
}

#pragma mark - Process JSON to Core Data
- (void)processJSONDataRecordsIntoCoreData {
    NSManagedObjectContext *managedObjectContext = [[LSCoreDataController sharedInstance] backgroundManagedObjectContext];
    for (NSString *className in self.registeredClassesToSync) {
        if (![self initialSyncComplete]) { // import all downloaded data to Core Data for initial sync
            NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
            NSArray *records = [JSONDictionary objectForKey:@"results"];
            for (NSDictionary *record in records) {
                [self newManagedObjectWithClassName:className forRecord:record];
            }
        } else {
            NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:@"objectId"];
            if ([downloadedRecords lastObject]) {
                NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:@"objectId" usingArrayOfIds:[downloadedRecords valueForKey:@"objectId"] inArrayOfIds:YES];
                int currentIndex = 0;
                for (NSDictionary *record in downloadedRecords) {
                    NSManagedObject *storedManagedObject = nil;
                    if ([storedRecords count] > currentIndex) {
                        storedManagedObject = [storedRecords objectAtIndex:currentIndex];
                    }
                    
                    if ([[storedManagedObject valueForKey:@"objectId"] isEqualToString:[record valueForKey:@"objectId"]]) {
                        [self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
                    } else {
                        [self newManagedObjectWithClassName:className forRecord:record];
                    }
                    currentIndex++;
                }
            }
        }
        
        [managedObjectContext performBlockAndWait:^{
            NSError *error = nil;
            if (![managedObjectContext save:&error]) {
                NSLog(@"Unable to save context for class %@", className);
            }
        }];
        
        [self deleteJSONDataRecordsForClassWithName:className];
    }
    
    [self executeSyncCompletedOperations];
//    [self downloadDataForRegisteredObjects:NO toDeleteLocalRecords:YES];
}

- (void)processJSONDataRecordsIntoCoreData:(NSString *)className toUseUpdatedAtDate:(BOOL)useUpdatedAtDate
{
    NSManagedObjectContext *managedObjectContext = [[LSCoreDataController sharedInstance] backgroundManagedObjectContext];
    if (!useUpdatedAtDate) { // import all downloaded data to Core Data for initial sync
        NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
        NSArray *records = [JSONDictionary objectForKey:@"results"];
        for (NSDictionary *record in records) {
            [self newManagedObjectWithClassName:className forRecord:record];
        }
    } else {
        NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:@"objectId"];
        if ([downloadedRecords lastObject]) {
            NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:@"objectId" usingArrayOfIds:[downloadedRecords valueForKey:@"objectId"] inArrayOfIds:YES];
            int currentIndex = 0;
            for (NSDictionary *record in downloadedRecords) {
                NSManagedObject *storedManagedObject = nil;
                if ([storedRecords count] > currentIndex) {
                    storedManagedObject = [storedRecords objectAtIndex:currentIndex];
                }
                
                if ([[storedManagedObject valueForKey:@"objectId"] isEqualToString:[record valueForKey:@"objectId"]]) {
                    [self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
                } else {
                    [self newManagedObjectWithClassName:className forRecord:record];
                }
                currentIndex++;
            }
        }
    }
    
    [managedObjectContext performBlockAndWait:^{
        NSError *error = nil;
        if (![managedObjectContext save:&error]) {
            NSLog(@"Unable to save context for class %@", className);
        }
    }];
    
    [self deleteJSONDataRecordsForClassWithName:className];
    [self syncCompletedForRegisteredObject:className toUseUpdatedAtDate:useUpdatedAtDate];
}

- (void)processJSONDataRecordsForDeletion
{
    NSManagedObjectContext *managedObjectContext = [[LSCoreDataController sharedInstance] backgroundManagedObjectContext];
    for (NSString *className in self.registeredClassesToSync) {
        NSArray *JSONRecords = [self JSONDataRecordsForClass:className sortedByKey:@"objectId"];
        if ([JSONRecords count] > 0) {
            NSArray *storedRecords = [self
                                      managedObjectsForClass:className
                                      sortedByKey:@"objectId"
                                      usingArrayOfIds:[JSONRecords valueForKey:@"objectId"]
                                      inArrayOfIds:NO];
            
            [managedObjectContext performBlockAndWait:^{
                for (NSManagedObject *managedObject in storedRecords) {
                    [managedObjectContext deleteObject:managedObject];
                }
                NSError *error = nil;
                BOOL saved = [managedObjectContext save:&error];
                if (!saved) {
                    NSLog(@"Unable to save context after deleting records for class %@ because %@", className, error);
                }
            }];
        }
        
        [self deleteJSONDataRecordsForClassWithName:className];
    }
    
    [self executeSyncCompletedOperations];
}

#pragma mark - Process ManagedObject
- (void)newManagedObjectWithClassName:(NSString *)className forRecord:(NSDictionary *)record {
    NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:className inManagedObjectContext:[[LSCoreDataController sharedInstance] backgroundManagedObjectContext]];
    [record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [self setValue:obj forKey:key forManagedObject:newManagedObject];
    }];
    [record setValue:[NSNumber numberWithInt:LSObjectSynced] forKey:@"syncStatus"];
}

- (void)updateManagedObject:(NSManagedObject *)managedObject withRecord:(NSDictionary *)record {
    [record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [self setValue:obj forKey:key forManagedObject:managedObject];
    }];
}

- (void)setValue:(id)value forKey:(NSString *)key forManagedObject:(NSManagedObject *)managedObject {
    if ([key isEqualToString:@"createdAt"] || [key isEqualToString:@"updatedAt"]) {
        NSDate *date = [self dateUsingStringFromAPI:value];
        [managedObject setValue:date forKey:key];
    } else {
        [managedObject setValue:value forKey:key];
    }
    
    /*Add Relationship Category - Book*/
    if ([key isEqualToString:@"categoryIds"] && [value isKindOfClass:[NSArray class]]) {
        NSArray *categoryIds = (NSArray *)value;
        for (NSNumber *categoryId in categoryIds) {
            NSPredicate *predicate = [NSPredicate predicateWithFormat:@"categoryId = %d", [categoryId intValue]];
            BookCategory *category = (BookCategory *)[BookCategory findObjectInContext:[[LSCoreDataController sharedInstance] backgroundManagedObjectContext] withPredicate:predicate];
            if (category && [managedObject isKindOfClass:[BookDetail class]]) {
                [category addListOfBooksObject:(BookDetail *)managedObject];
            }
        }
    }
}

- (NSArray *)managedObjectsForClass:(NSString *)className withSyncStatus:(LSObjectSyncStatus)syncStatus {
    __block NSArray *results = nil;
    NSManagedObjectContext *managedObjectContext = [[LSCoreDataController sharedInstance] backgroundManagedObjectContext];
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"syncStatus = %d", syncStatus];
    [fetchRequest setPredicate:predicate];
    [managedObjectContext performBlockAndWait:^{
        NSError *error = nil;
        results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    }];
    
    return results;
}

- (NSArray *)managedObjectsForClass:(NSString *)className sortedByKey:(NSString *)key usingArrayOfIds:(NSArray *)idArray inArrayOfIds:(BOOL)inIds {
    __block NSArray *results = nil;
    NSManagedObjectContext *managedObjectContext = [[LSCoreDataController sharedInstance] backgroundManagedObjectContext];
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
    NSPredicate *predicate;
    if (inIds) {
        predicate = [NSPredicate predicateWithFormat:@"objectId IN %@", idArray];
    } else {
        predicate = [NSPredicate predicateWithFormat:@"NOT (objectId IN %@)", idArray];
    }
    
    [fetchRequest setPredicate:predicate];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:
                                      [NSSortDescriptor sortDescriptorWithKey:@"objectId" ascending:YES]]];
    [managedObjectContext performBlockAndWait:^{
        NSError *error = nil;
        results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    }];
    
    return results;
}

#pragma mark - Sync Completed Callback
- (void)syncCompletedForRegisteredObject:(NSString *)className toUseUpdatedAtDate:(BOOL)useUpdatedAtDate
{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSError *error = nil;
        [[LSCoreDataController sharedInstance] saveBackgroundContext];
        if (error) {
            NSLog(@"Error saving background context after creating objects on server: %@", error);
        }
        
        [[LSCoreDataController sharedInstance] saveMasterContext];
        if ([self.delegate respondsToSelector:@selector(syncEngine:forRegisteredObject:toUseUpdatedAtDate:)]) {
            [self.delegate syncEngine:self forRegisteredObject:className toUseUpdatedAtDate:useUpdatedAtDate];
        }
        [self willChangeValueForKey:@"syncInProgress"];
        _syncInProgress = NO;
        [self didChangeValueForKey:@"syncInProgress"];
    });
}

- (void)executeSyncCompletedOperations {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setInitialSyncCompleted];
        NSError *error = nil;
        [[LSCoreDataController sharedInstance] saveBackgroundContext];
        if (error) {
            NSLog(@"Error saving background context after creating objects on server: %@", error);
        }
        
        [[LSCoreDataController sharedInstance] saveMasterContext];
        for (NSString *className in self.registeredClassesToSync) {
            [[NSNotificationCenter defaultCenter]
             postNotificationName:kLSSyncEngineSyncCompletedNotificationName
             object:className];
        }
        
        [self willChangeValueForKey:@"syncInProgress"];
        _syncInProgress = NO;
        [self didChangeValueForKey:@"syncInProgress"];
    });
}

- (BOOL)initialSyncComplete {
    return [[[NSUserDefaults standardUserDefaults] valueForKey:kLSSyncEngineInitialCompleteKey] boolValue];
}

- (void)setInitialSyncCompleted {
    [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:YES] forKey:kLSSyncEngineInitialCompleteKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

#pragma mark - UpdateAtAfterData
- (NSDate *)mostRecentUpdatedAtDateForEntityWithName:(NSString *)entityName {
    __block NSDate *date = nil;
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
    [request setSortDescriptors:[NSArray arrayWithObject:
                                 [NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]];
    [request setFetchLimit:1];
    [[[LSCoreDataController sharedInstance] backgroundManagedObjectContext] performBlockAndWait:^{
        NSError *error = nil;
        NSArray *results = [[[LSCoreDataController sharedInstance] backgroundManagedObjectContext] executeFetchRequest:request error:&error];
        if ([results lastObject])   {
            date = [[results lastObject] valueForKey:@"updatedAt"];
        }
    }];
    
    return date;
}

- (NSDate *)dateUsingStringFromAPI:(NSString *)dateString
{
    // NSDateFormatter does not like ISO 8601 so strip the milliseconds and timezone
    dateString = [dateString substringWithRange:NSMakeRange(0, [dateString length]-5)];
    
    return [[kAppDelegate dateFormatter] dateFromString:dateString];
}
#pragma mark - File Management
- (NSURL *)applicationCacheDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject];
}

- (NSURL *)JSONDataRecordsDirectory
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *url = [NSURL URLWithString:@"JSONRecords/" relativeToURL:[self applicationCacheDirectory]];
    NSError *error = nil;
    if (![fileManager fileExistsAtPath:[url path]]) {
        [fileManager createDirectoryAtPath:[url path] withIntermediateDirectories:YES attributes:nil error:&error];
    }
    
    return url;
}

- (void)writeJSONResponse:(id)response toDiskForClassWithName:(NSString *)className
{
    NSURL *fileURL = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]];
    if (![(NSDictionary *)response writeToFile:[fileURL path] atomically:YES]) {
        NSLog(@"Error saving response to disk, will attempt to remove NSNull values and try again.");
        // remove NSNulls and try again...
        NSArray *records = [response objectForKey:@"results"];
        NSMutableArray *nullFreeRecords = [NSMutableArray array];
        for (NSDictionary *record in records) {
            NSMutableDictionary *nullFreeRecord = [NSMutableDictionary dictionaryWithDictionary:record];
            [record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                if ([obj isKindOfClass:[NSNull class]]) {
                    [nullFreeRecord setValue:nil forKey:key];
                }
            }];
            [nullFreeRecords addObject:nullFreeRecord];
        }
        
        NSDictionary *nullFreeDictionary = [NSDictionary dictionaryWithObject:nullFreeRecords forKey:@"results"];
        
        if (![nullFreeDictionary writeToFile:[fileURL path] atomically:YES]) {
            NSLog(@"Failed all attempts to save reponse to disk: %@", response);
        }
    }
}

- (void)deleteJSONDataRecordsForClassWithName:(NSString *)className
{
    NSURL *url = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]];
    NSError *error = nil;
    BOOL deleted = [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
    if (!deleted) {
        NSLog(@"Unable to delete JSON Records at %@, reason: %@", url, error);
    }
}

- (NSDictionary *)JSONDictionaryForClassWithName:(NSString *)className
{
    NSURL *fileURL = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]];
    return [NSDictionary dictionaryWithContentsOfURL:fileURL];
}

- (NSArray *)JSONDataRecordsForClass:(NSString *)className sortedByKey:(NSString *)key
{
    NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
    NSArray *records = [JSONDictionary objectForKey:@"results"];
    return [records sortedArrayUsingDescriptors:[NSArray arrayWithObject:
                                                 [NSSortDescriptor sortDescriptorWithKey:key ascending:YES]]];
}

@end
